Kubernetes 源码笔记(kubeadm)

在 Kubernetes 发展初期,部署 Kubernetes 一直是一件让初学者头疼的事情,Kubernetes 也开始重视这个问题,2017年,在社区志愿者的推动下,社区发起了一个独立的部署 Kubernetes 的项目,kubeadm

经过一年多的发展,kubeadm 已经可以一键式进行 Kubernetes 集群的快速初始化和安装,极大地简化了部署过程。值得一提的是,在很长一段时间里 kubeadm 有个比较欠缺的地方是无法做到一键部署一个高可用的 Kubernetes 集群,这是 kubeadm 目前的工作重点,好在这个功能已经在 1.11 版本刚刚发布,可以参考 Creating Highly Available Clusters with kubeadm

原理

Kubernetes 是一个管理容器的平台,所以用容器来部署自身的组件是一件自然而然的事情,kube-apiserver, scheduler 等组件还好解决,比较麻烦的是 kubelet 组件,因为 kubelet 需要承担与容器运行时交互的工作,还需要解决容器运行时网络,存储的配置问题,这些都需要直接操作宿主机的,如果 kubelet 本身就运行在容器里,这些问题倒也不是不能解决,只是需要做各种方式的 hack,部署 Kubernetes 的这个需求会变得更为复杂。kubeadm 的方案是 kubelet 直接运行在宿主机上,其他的 Kubernetes 组件使用容器部署。

所以使用 kubeadm 的第一步手动安装 kubeadm,kubelet,kubectl 这3个二进制文件,可以参考我的 k8s-digitalocean-terraform 项目,里面有需要安装的依赖。
这是运行 kubeadm 的前提。

基本流程

kubeadm 中比较关键的工作流程就两步,1. kubeadm init 进行初始化。2. kubeadm join master-ip:master:port 将 worker 节点连接到 master 节点,组成集群。

kubeadm init

以下内容均考虑使用默认参数的情况。

kubeadm init 首先会做一系列的检查工作,以确保当前机器是可以部署 Kubernetes 的。这一步叫做 Preflight Checks,包括:

  • Linux 内核版本是否在 3.10 以上
  • Linux cgroups 是否可用
  • hostname 是否符合 DNS 命名规范(RFC 1123)
  • kubeadm 和 kubelet 版本是否匹配
  • 是否安装了 Kubernetes 的二进制文件
  • 10250,10251,10252 端口是否可用
  • ip、mount 工具是否已经存在
  • 是否安装了 Docker

接着 kubeadm 会生成 Kubernetes 对外提供服务所需的安全证书。

这个过程完成后,kubeadm 会为 Master 组件生成 Pod 的 yaml 格式的配置文件,存放在 /etc/kubernetes/manifests,kubelet 会使用一种叫做 Static Pod 的特殊的容器启动方法,根据 yaml 格式文件在机器上启动 kube-apiserver、kube-controller-manager、kube-scheduler、etcd 这四个组件。kubeadm 可以根据配置使用已有的 etcd 集群,不一定非得自己创建。

kubeadm 会监控 /etc/kubernetes/manifests 目录下的内容,一旦生成 yaml 文件,kubelet 就会创建这些 yaml 定义的 Pod,Master 节点的各个组件启动之后,kubeadm 会检查 localhost:6443/healthz 健康状态,直到 Master 节点完全运行起来。

再接下来 kubeadm 会为集群生成一个 bootstrap token,worker 节点可以通过这个 token 加入到集群里。生成 token 后,master 节点的重要信息会通过 cluster-info 这个 ConfigMap 保存到 etcd 里。

最后 kubeadm 会安装插件,有两个默认的必装的插件,一个是 kube-proxy,默认使用 iptable,未来 ipvs 会是默认选项;另一个是 DNS,默认安装 kubedns + dnsmsaq,未来趋势是转向 CoreDNS。插件会通过 Kubernetes 客户端进行创建。

kubeadm join

其他节点加入到 Master 节点需要保证机器之间是网络互通的,这是将来容器之间网络互通的前提。执行完 kubeadm init 之后可以看到 kubeadm join 的使用方法,在其他机器上运行这行命令即可。

kubeadm join 也会检测依赖环境,相比于 kubeadm init 要简单得多,通过检测时候,根据 token 从 kube-apiserver 拿到 cluster-info,获取 kube-apiserver 的证书,这时 kubelet 就接入了集群中。

如果机器很多,手工操作当然不酷,自动化才是未来,可以参考一下 结合 Terraform 在 DigitalOcean 上一键式部署 Kubernetes 这个项目。

代码解析

上面已经把流程描述了一遍,可以看出来,就代码实现来说,不会有什么很复杂的东西,其实代码里也真的是一股脑一个流程走到底,尽管如此我们还是继续研读一下代码,挖掘一些有价值的东西。

基于 v1.10.0-rc.1 代码版本进行分析。

kubeadm init

init 方法的入口在 cmd/kubeadm/app/cmd/init.go

在 cobra 对应的 init 子命令下,先调用 NewInit 方法,进行 Preflight Checks 操作,确保本机可以运行 kubeadm,接着调用 InitRun 方法进行 Master 节点的初始化操作。

系统状态检查

NewInit 里会调用 RunInitMasterChecks 用来完成安装前的检测,这部分内容都在 cmd/kubeadm/preflight/checks.go 里。核心逻辑是定义一堆 checks,然后进行变量,全部通过则满足 Preflight Checks 的要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
checks := []Checker{
KubernetesVersionCheck{KubernetesVersion: cfg.KubernetesVersion, KubeadmVersion: kubeadmversion.Get().GitVersion},
SystemVerificationCheck{CRISocket: cfg.CRISocket},
IsPrivilegedUserCheck{},
HostnameCheck{nodeName: cfg.NodeName},
KubeletVersionCheck{KubernetesVersion: cfg.KubernetesVersion, exec: execer},
ServiceCheck{Service: "kubelet", CheckIfActive: false},
// ...
FirewalldCheck{ports: []int{int(cfg.API.BindPort), 10250}},
PortOpenCheck{port: int(cfg.API.BindPort)},
// ...
FileAvailableCheck{Path: kubeadmconstants.GetStaticPodFilepath(kubeadmconstants.KubeAPIServer, manifestsDir)},
// ...
InPathCheck{executable: "ip", mandatory: true, exec: execer},
// ...
criCtlChecker,
ExtraArgsCheck{
APIServerExtraArgs: cfg.APIServerExtraArgs,
ControllerManagerExtraArgs: cfg.ControllerManagerExtraArgs,
SchedulerExtraArgs: cfg.SchedulerExtraArgs,
},
HTTPProxyCheck{Proto: "https", Host: cfg.API.AdvertiseAddress},
// ...
}

生成证书

生成证书的操作在 InitRun 方法里的 PHASE 1。调用 certsphase.CreatePKIAssets 实现。

这部分代码在 cmd/kubeadm/app/cmd/phases/certs.go 里,依次调用了一下函数:

1
2
3
4
5
6
7
8
9
10
11
CreateCACertAndKeyFiles,
CreateAPIServerCertAndKeyFiles,
CreateAPIServerKubeletClientCertAndKeyFiles,
CreateEtcdCACertAndKeyFiles,
CreateEtcdServerCertAndKeyFiles,
CreateEtcdPeerCertAndKeyFiles,
CreateEtcdHealthcheckClientCertAndKeyFiles,
CreateAPIServerEtcdClientCertAndKeyFiles,
CreateServiceAccountKeyAndPublicKeyFiles,
CreateFrontProxyCACertAndKeyFiles,
CreateFrontProxyClientCertAndKeyFiles,

为确保安全,Kubernetes 的各个组件需要使用 x509 证书对通信进行加密和认证,如果是使用手动安装的话,整个过程很复杂,所以 kbeadm 将这个过程自动化了。

首先通过 CreateCACertAndKeyFiles 创建 CA,用来签名后续创建的其他证书,对证书进行管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func CreateCACertAndKeyFiles(cfg *kubeadmapi.MasterConfiguration) error {
caCert, caKey, err := NewCACertAndKey()
if err != nil {
return err
}
return writeCertificateAuthorithyFilesIfNotExist(
cfg.CertificatesDir,
kubeadmconstants.CACertAndKeyBaseName,
caCert,
caKey,
)
}

func NewCACertAndKey() (*x509.Certificate, *rsa.PrivateKey, error) {
caCert, caKey, err := pkiutil.NewCertificateAuthority()
if err != nil {
return nil, nil, fmt.Errorf("failure while generating CA certificate and key: %v", err)
}
return caCert, caKey, nil
}

pkiutil 模块的 NewCertificateAuthoritycmd/kubeadm/app/phases/certs/pkiutil/pki_helpers.go 里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func NewCertificateAuthority() (*x509.Certificate, *rsa.PrivateKey, error) {
key, err := certutil.NewPrivateKey()
if err != nil {
return nil, nil, fmt.Errorf("unable to create private key [%v]", err)
}

config := certutil.Config{
CommonName: "kubernetes",
}
cert, err := certutil.NewSelfSignedCACert(config, key)
if err != nil {
return nil, nil, fmt.Errorf("unable to create self-signed certificate [%v]", err)
}

return cert, key, nil
}

私钥通过 k8s.io/client-go/util/cert 包里的函数实现,其中 NewPrivateKey 只是对 rsa 库函数的封装。

CreateAPIServerCertAndKeyFiles 为例,我们再看 kube-apiserver 的证书是怎么生成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
func CreateAPIServerCertAndKeyFiles(cfg *kubeadmapi.MasterConfiguration) error {
caCert, caKey, err := loadCertificateAuthority(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName)
if err != nil {
return err
}
apiCert, apiKey, err := NewAPIServerCertAndKey(cfg, caCert, caKey)
if err != nil {
return err
}
return writeCertificateFilesIfNotExist(
cfg.CertificatesDir,
kubeadmconstants.APIServerCertAndKeyBaseName,
caCert,
apiCert,
apiKey,
)
}

func NewAPIServerCertAndKey(cfg *kubeadmapi.MasterConfiguration, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) {

altNames, err := pkiutil.GetAPIServerAltNames(cfg)
if err != nil {
return nil, nil, fmt.Errorf("failure while composing altnames for API server: %v", err)
}

config := certutil.Config{
CommonName: kubeadmconstants.APIServerCertCommonName,
AltNames: *altNames,
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
apiCert, apiKey, err := pkiutil.NewCertAndKey(caCert, caKey, config)
if err != nil {
return nil, nil, fmt.Errorf("failure while creating API server key and certificate: %v", err)
}

return apiCert, apiKey, nil
}

func NewCertAndKey(caCert *x509.Certificate, caKey *rsa.PrivateKey, config certutil.Config) (*x509.Certificate, *rsa.PrivateKey, error) {
key, err := certutil.NewPrivateKey()
if err != nil {
return nil, nil, fmt.Errorf("unable to create private key [%v]", err)
}

cert, err := certutil.NewSignedCert(config, key, caCert, caKey)
if err != nil {
return nil, nil, fmt.Errorf("unable to sign certificate [%v]", err)
}

return cert, key, nil
}

大致流程和生成 ca 时类似,只是进行配置时需要生成 AltName,这是 master 的地址;最后生成证书是需要用 ca 添加数字签名,生成证书。

生成 master 各组件所需的 kubeconfig

这部分代码在 cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go 里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func CreateInitKubeConfigFiles(outDir string, cfg *kubeadmapi.MasterConfiguration) error {
return createKubeConfigFiles(
outDir,
cfg,
kubeadmconstants.AdminKubeConfigFileName,
kubeadmconstants.KubeletKubeConfigFileName,
kubeadmconstants.ControllerManagerKubeConfigFileName,
kubeadmconstants.SchedulerKubeConfigFileName,
)
}

func createKubeConfigFiles(outDir string, cfg *kubeadmapi.MasterConfiguration, kubeConfigFileNames ...string) error {
specs, err := getKubeConfigSpecs(cfg)
if err != nil {
return err
}

for _, kubeConfigFileName := range kubeConfigFileNames {
spec, exists := specs[kubeConfigFileName]
if !exists {
return fmt.Errorf("couldn't retrive KubeConfigSpec for %s", kubeConfigFileName)
}

config, err := buildKubeConfigFromSpec(spec)
if err != nil {
return err
}

if err = createKubeConfigFileIfNotExists(outDir, kubeConfigFileName, config); err != nil {
return err
}
}

return nil
}

首先根据配置获取 spec,然后根据根据 spec 依次生成 admin.conf, controller-manager.conf, kubelet.conf, scheduler.conf 文件。

生成 Static Pod 静态文件

前面已经提过原理,kubelet 会监控 /etc/kubernetes/manifests 目录,加载所有的 Pod Yaml 文件,然后在本机上启动这些 Pod。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func createStaticPodFiles(manifestDir string, cfg *kubeadmapi.MasterConfiguration, componentNames ...string) error {
k8sVersion, err := version.ParseSemantic(cfg.KubernetesVersion)
if err != nil {
return err
}

specs := GetStaticPodSpecs(cfg, k8sVersion)

for _, componentName := range componentNames {
spec, exists := specs[componentName]
if !exists {
return fmt.Errorf("couldn't retrive StaticPodSpec for %s", componentName)
}

if err := staticpodutil.WriteStaticPodToDisk(componentName, manifestDir, spec); err != nil {
return fmt.Errorf("failed to create static pod manifest file for %q: %v", componentName, err)
}

fmt.Printf("[controlplane] Wrote Static Pod manifest for component %s to %q\n", componentName, kubeadmconstants.GetStaticPodFilepath(componentName, manifestDir))
}

return nil
}

如果没有配置外部的 etcd 集群,默认情况下也会根据这种方式创建 etcd。原理一样,不过代码在 cmd/kubeadm/app/phases/etcd/local.go

等待 master 启动

接下来的操作需要等待 master 的组件完全启动后才能进行,这一步需要拉取 kube-apiserver, kube-scheduler, kube-controller-manager, etcd 的镜像,可能会花费比较长的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func waitForAPIAndKubelet(waiter apiclient.Waiter) error {
errorChan := make(chan error)

fmt.Printf("[init] Waiting for the kubelet to boot up the control plane as Static Pods from directory %q.\n", kubeadmconstants.GetStaticPodDirectory())
fmt.Println("[init] This might take a minute or longer if the control plane images have to be pulled.")

go func(errC chan error, waiter apiclient.Waiter) {
if err := waiter.WaitForHealthyKubelet(40*time.Second, "http://localhost:10255/healthz"); err != nil {
errC <- err
}
}(errorChan, waiter)

go func(errC chan error, waiter apiclient.Waiter) {
if err := waiter.WaitForHealthyKubelet(60*time.Second, "http://localhost:10255/healthz/syncloop"); err != nil {
errC <- err
}
}(errorChan, waiter)

go func(errC chan error, waiter apiclient.Waiter) {
errC <- waiter.WaitForAPI()
}(errorChan, waiter)

return <-errorChan
}

kubeadm 会不断检查 http://localhost:6443/healthz 这个健康检查的接口 以及 http://localhost:10255/healthz, http://localhost:10255/healthz/syncloop 这两个 kubelet 的可读接口,等待 Master 组件完全运行起来。

存储 ConfigMap

这一步没什么特别的,通过 api-client 把 Master 的配置上传到 Kubernetes 的 ConfigMap。代码在 cmd/kubeadm/app/phases/uploadconfig/uploadconfig.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func UploadConfiguration(cfg *kubeadmapi.MasterConfiguration, client clientset.Interface) error {
fmt.Printf("[uploadconfig] Storing the configuration used in ConfigMap %q in the %q Namespace\n", kubeadmconstants.MasterConfigurationConfigMap, metav1.NamespaceSystem)

externalcfg := &kubeadmapiext.MasterConfiguration{}
legacyscheme.Scheme.Convert(cfg, externalcfg, nil)

externalcfg.Token = ""

cfgYaml, err := yaml.Marshal(*externalcfg)
if err != nil {
return err
}

return apiclient.CreateOrUpdateConfigMap(client, &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: kubeadmconstants.MasterConfigurationConfigMap,
Namespace: metav1.NamespaceSystem,
},
Data: map[string]string{
kubeadmconstants.MasterConfigurationConfigMapKey: string(cfgYaml),
},
})
}

其中 apiclient 是 kubeadm 对 api-client 的简单封装,代码在 cmd/kubeadm/app/util/apiclient/idempotency.go

创建 token

生成的这个 token 让 worker 节点有权限可以拿到 cluster-info 这个 ConfigMap 里的信息,包括 api-server 的地址,认证信息等。

这部分代码在 cmd/kubeadm/app/phases/bootstraptoken/node/token.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
func UpdateOrCreateToken(client clientset.Interface, token string, failIfExists bool, tokenDuration time.Duration, usages []string, extraGroups []string, description string) error {
tokenID, tokenSecret, err := tokenutil.ParseToken(token)
if err != nil {
return err
}
secretName := fmt.Sprintf("%s%s", bootstrapapi.BootstrapTokenSecretPrefix, tokenID)
var lastErr error
for i := 0; i < tokenCreateRetries; i++ {
secret, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Get(secretName, metav1.GetOptions{})
if err == nil {
if failIfExists {
return fmt.Errorf("a token with id %q already exists", tokenID)
}
tokenSecretData, err := encodeTokenSecretData(tokenID, tokenSecret, tokenDuration, usages, extraGroups, description)
if err != nil {
return err
}
secret.Data = tokenSecretData
if _, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Update(secret); err == nil {
return nil
}
lastErr = err
continue
}

if apierrors.IsNotFound(err) {
tokenSecretData, err := encodeTokenSecretData(tokenID, tokenSecret, tokenDuration, usages, extraGroups, description)
if err != nil {
return err
}

secret = &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
},
Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken),
Data: tokenSecretData,
}
if _, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Create(secret); err == nil {
return nil
}
lastErr = err
continue
}

}
return fmt.Errorf(
"unable to create bootstrap token after %d attempts [%v]",
tokenCreateRetries,
lastErr,
)
}

通过 Kubernetes 的 secret 创建。

安装 dns,kube-proxy 插件

dns,kube-proxy 是 Kubernetes 里默认的,必装的插件。

coreDNS 的 Deployment, ConfigMap, ClusterRole 等 yaml 配置直接用字符串定义在 cmd/kubeadm/app/phases/addons/dns/manifests.go 文件里。

代码基本就是根据配置渲染模板,通过 Kubernetes 的 api 创建,没什么特别的。

kube-proxy 的情况类似,yaml 配置在 cmd/kubeadm/app/phases/addons/proxy/manifests.go 文件里定义。

kubeadm join

join 方法跟 init 方法类似,但要简单得多,首先调用 NewJoin,先调用 RunJoinNodeChecks 来完成初始化前的检测,然后调用 Run 方法将本节点加入到集群里。

系统状态检查

worker 节点的 preflight checks 跟 master 节点的略微不同。

1
2
3
4
5
6
7
8
9
10
11
checks := []Checker{
SystemVerificationCheck{CRISocket: cfg.CRISocket},
IsPrivilegedUserCheck{},
HostnameCheck{cfg.NodeName},
KubeletVersionCheck{exec: execer},
ServiceCheck{Service: "kubelet", CheckIfActive: false},
PortOpenCheck{port: 10250},
DirAvailableCheck{Path: filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ManifestsSubDirName)},
FileAvailableCheck{Path: cfg.CACertPath},
// ...
}

除了一些常规的检测外,worker 节点还需要确保 ip, ipables, mount 命令已经安装好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if runtime.GOOS == "linux" {
checks = append(checks,
FileContentCheck{Path: bridgenf, Content: []byte{'1'}},
SwapCheck{},
InPathCheck{executable: "ip", mandatory: true, exec: execer},
InPathCheck{executable: "iptables", mandatory: true, exec: execer},
InPathCheck{executable: "mount", mandatory: true, exec: execer},
InPathCheck{executable: "nsenter", mandatory: true, exec: execer},
InPathCheck{executable: "ebtables", mandatory: false, exec: execer},
InPathCheck{executable: "ethtool", mandatory: false, exec: execer},
InPathCheck{executable: "socat", mandatory: false, exec: execer},
InPathCheck{executable: "tc", mandatory: false, exec: execer},
InPathCheck{executable: "touch", mandatory: false, exec: execer},
criCtlChecker)
}

加入集群

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (j *Join) Run(out io.Writer) error {
cfg, err := discovery.For(j.cfg)
if err != nil {
return err
}

kubeconfigFile := filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.KubeletBootstrapKubeConfigFileName)

if err := kubeconfigutil.WriteToDisk(kubeconfigFile, cfg); err != nil {
return fmt.Errorf("couldn't save bootstrap-kubelet.conf to disk: %v", err)
}
cluster := cfg.Contexts[cfg.CurrentContext].Cluster
err = certutil.WriteCert(j.cfg.CACertPath, cfg.Clusters[cluster].CertificateAuthorityData)
if err != nil {
return fmt.Errorf("couldn't save the CA certificate to disk: %v", err)
}

if features.Enabled(j.cfg.FeatureGates, features.DynamicKubeletConfig) {
if err := kubeletphase.ConsumeBaseKubeletConfiguration(j.cfg.NodeName); err != nil {
return fmt.Errorf("error consuming base kubelet configuration: %v", err)
}
}

fmt.Fprintf(out, joinDoneMsgf)
return nil
}

join 命令会调用 func (j *Join) Run(out io.Writer) error 方法,先根据 token 获取 cluster-info 里的信息,写入 bootstrap-kubelet.conf 文件,然后向 Master 提交一个 certificate signing request (CSR),通过后 worker 节点保存 ca.crtkubelet.conf 文件,bootstrap-kubelet.conf 会被删除,这时应用 kubelet.conf 本节点就加入了集群。

总结

kubeadm 是 Kubernetes 项目的原生部署工具,使用 kubeadm 部署一个 Kubernetes 集群,对于理解 Kubernetes 组件的架构很有帮助。从实现来看,kubeadm 简洁明了,对于最新版的 Kubernetes 支持很好,而且和 k8s 一样是用 Golang 写的,只需要运行一个二进制文件就行,很轻便。深入代码,我们会发现 kubeadm 完全没有如何考虑部署高可用的 Kubernetes 集群,比如创建 kube-proxy 不能配置 kube-apiserver 的地址;运行 kubeadm join 时不能对 kube-apiserver 地址进行配置。几乎没有实现高可用的扩展性,这一功能的缺失使得整个社区诞生了很多的更复杂的部署工具,有的基于 Ansible,有的是提供了一堆 shell 脚本,还有的基于 kubeadm 做修改,有的甚至用 Golang 重新写了一套部署工具,百花齐放,只是质量参差不齐,光是调研这些工具就需要很多成本和精力。值得期待的是,kubeadm 在往『部署高可用集群』这个方向努力,等到这个特性稳定之后有机会我们再研读一下代码。

References